/*
获取accesstoken
是公众号的 全局唯一 接口 调用凭据
特点：
需要512空间
有效期2小时
接口次数2000次
 */
//引入 request-promise-native
const rq=require('request-promise-native');
//解决下载临时素材 是流的形式 怎么把流的形式转化回来  rq库没法处理流，只能使用request
const rquest=require('request');

//fs 模块
const {createReadStream,createWriteStream} = require('fs');
// path模块
const {resolve,join}=require('path');

const {appID,appsecret}=require('../config');
// 引入menu模块
const menu=require('./menu');
//引入api模块
const api=require('../utils/api');
// 引入工具函数
const {writeFileAsync,readFileAsync}=require('../utils/tool');

class Wechat {
    constructor(){

    }
    /*
      用来获取access_token
     */
    getAccessToken (){
        const url=`${api.accessToken}&appid=${appID}&secret=${appsecret}`
        // console.log(url)
        //发送请求，服务器端么法使用ajax，我们使用一些库来实现
        /*
        request
        request-promise-native 这个库依赖于第一个库，所以都要下载.返回值是一个
        promise对象
         */
        return new Promise((resolve,reject)=>{
            rq({
                method:'GET',
                url,
                json:true
            }).then(res=>{
                // console.log(res)
                res.expires_in=Date.now()+(res.expires_in-300)*1000;
                //将promise的对象状态改为成功的状态
                resolve(res)
            }).catch(res=>{
                // console.log(res)
                //将promise的对象状态改为失败的状态
                reject('getAccessToken出了问题'+res)
            })
        })
    }

    /*
    save用来保存 access_token 的方法
    @param accessToken 用来保存的凭据
     */
    saveAccessToken(accessToken){
        return writeFileAsync(accessToken,'accessToken.txt')
    }

    /*
    read用来读取 access_token 的方法
     */
    readAccessToken(){
      return  readFileAsync('accessTocken.txt');
    }

    /*
     accessToken 是否有效
     @param data
     */
    isValidAccessToken (data){
        // 检测传入的参数是否有效
        //  是否有数据   数据是否有token    数据是否有时间
        if(!data && !data.access_token && !data.expires_in){
            return false;
        }

         //检测access_token 是否在有效期内
         if(data.expires_in< Date.now()){
             // 过期了
             return false;
         }else{
             //没有过期
             return true;
         }
    }


    /*
    fetchAccessToken
    用来获取没有过期的access_token
    @return {Promise<any>} access_token
     */
    fetchAccessToken (){
        //先判断 对象上 是否有token 和过期时间 和 这个token是否有效
        if(this.access_token && this.expires_in && this.isValidAccessToken(this)){
        // 如果有效 就返回一个 promise的成功状态 自己定义一个对象，不直接返回this，因为this上东西太多
            return Promise.resolve({
                access_token:this.access_token,
                expires_in:this.expires_in
            })
        }
        // 这是 fetchAccessToken最终的返回值
       return this.readAccessToken()
                .then(async res=>{
                    //本地有文件
                    //判断文件是否过期
                    if(this.isValidAccessToken(res)){
                        //没过期
                       return Promise.resolve(res)
                    }else{
                        //过期了
                        // 发送请求获取access_token
                        let res=await this.getAccessToken()
                        //保存下来 本地文件
                        await this.saveAccessToken(res)
                        //将请求回来的access_token 返回出去
                        return Promise.resolve(res)
                    }
                })
                .catch(async err=>{
                    //本地没有文件
                    //过期了
                    // 发送请求获取access_token
                    let res=await this.getAccessToken()
                    //保存下来 本地文件
                    await this.saveAccessToken(res)
                    //将请求回来的access_token 返回出去
                    return Promise.resolve(res)
                })
                .then(res=>{
                    //将access_token挂载到this上
                    this.access_token=res.access_token;
                    this.expires_in=res.expires_in;
                    //返回res包装了一层promise对象（此对象是成功的状态）
                    //是this.readAccessToken()最终的返回值
                    return Promise.resolve(res)
                })
    }


    /*
      用来获取 jsapi_ticlet
     */
    getTicket (){
        //发送请求，服务器端么法使用ajax，我们使用一些库来实现
        /*
        request
        request-promise-native 这个库依赖于第一个库，所以都要下载.返回值是一个
        promise对象
         */
        return new Promise(async (resolve,reject)=>{
            //获取accessToken
            let data=await this.fetchAccessToken();
            const url=`${api.ticket}&ACCESS_TOKEN=${data.access_token}`;
console.log(url)
            rq({
                method:'GET',
                url,
                json:true
            }).then(res=>{
                // console.log(res)
                res.expires_in=Date.now()+(res.expires_in-300)*1000;
                //将promise的对象状态改为成功的状态
                resolve({
                    ticket:res.ticket,
                    expires_in:res.expires_in
                })
            }).catch(res=>{
                // console.log(res)
                //将promise的对象状态改为失败的状态
                reject('getTicket出了问题'+res)
            })
        })
    }

    /*
    save用来保存 ticket 的方法
    @param accessToken 用来保存的凭据
     */
    saveTicket(ticket){
        return writeFileAsync(ticket,'ticket.txt')
    }

    /*
    read用来读取 ticket 的方法
     */
    readTicket(){
        return  readFileAsync('ticket.txt');
    }

    /*
     ticket 是否有效
     @param data
     */
    isValidTicket (data){
        // 检测传入的参数是否有效
        //  是否有数据   数据是否有token    数据是否有时间
        if(!data && !data.ticket && !data.expires_in){
            return false;
        }

        //检测access_token 是否在有效期内
        if(data.expires_in< Date.now()){
            // 过期了
            return false;
        }else{
            //没有过期
            return true;
        }
    }


    /*
    fetchTicket
    用来获取没有过期的ticket
    @return {Promise<any>} ticket
     */
    fetchTicket (){
        //先判断 对象上 是否有token 和过期时间 和 这个token是否有效
        if(this.ticket && this.ticket_expires_in && this.isValidTicket(this)){
            // 如果有效 就返回一个 promise的成功状态 自己定义一个对象，不直接返回this，因为this上东西太多
            return Promise.resolve({
                ticket:this.ticket,
                expires_in:this.ticket_expires_in
            })
        }
        // 这是 fetchTicket最终的返回值
        return this.readTicket()
            .then(async res=>{
                //本地有文件
                //判断文件是否过期
                if(this.isValidTicket(res)){
                    //没过期
                    return Promise.resolve(res)
                }else{
                    //过期了
                    // 发送请求获取access_token
                    let res=await this.getTicket()
                    //保存下来 本地文件
                    await this.saveTicket(res)
                    //将请求回来的access_token 返回出去
                    return Promise.resolve(res)
                }
            })
            .catch(async err=>{
                //本地没有文件
                //过期了
                // 发送请求获取access_token
                let res=await this.getTicket()
                //保存下来 本地文件
                await this.saveTicket(res)
                //将请求回来的access_token 返回出去
                return Promise.resolve(res)
            })
            .then(res=>{
                //将access_token挂载到this上
                this.ticket=res.ticket;
                this.ticket_expires_in=res.expires_in;
                //返回res包装了一层promise对象（此对象是成功的状态）
                //是this.readTicket()最终的返回值
                return Promise.resolve(res)
            })
    }


    /*
      用来创建自定义菜单
      @param menu
      @return {promise <any>}
     */
    creatMenu (menu) {
        /*
         发送请求 是异步的方法
         为了保证它能够顺利的执行下去，执行完之后再执行下面的
         我们套路就是包装一层promise对象
        */
        return new Promise(async (resolve,reject)=>{
            try{
                //获取 access_token
                let data =await this.fetchAccessToken();
                // console.log(data)
                const url=`${api.menu.create}access_token=${data.access_token}`;

                // rq 库 发请求
                let result=await rq({
                    method:'POST',
                    url,
                    json:true,
                    body:menu //请求体
                })

                resolve(result);
            }catch (e) {
                reject('creatMenu方法出了问题：'+e)
            }
        })
    }
    /*
          用来删除你自定义菜单
          @return {promise <any>}
         */
    deleteMenu (){
        return new Promise(async (resolve,reject)=>{
            try {
                //获取 access_token
                let data =await this.fetchAccessToken();
                //定义请求地址
                let url=`${api.menu.delete}access_token=${data.access_token}`
                // console.log(url)
                //发送请求
                const  result=await rq({
                    method:'GET',
                    url,
                    json:true
                })
                resolve(result)
            }catch (e) {
                reject('deleteMenu方法出了问题：'+e)
            }
        })
    }

    // 上传临时素材
     uploadTemporaryMaterial (type,fileName){
        // 找到文件路径
         /*
         __dirname 总是指向被执行 js 文件的绝对路径
         resolve(a,b,c) 相当于 a/b/c
         我们这个例子 就是找到wechart.js 在会退一层找到media 在找到media下面的fileName
          */
        const filePath=resolve(__dirname,'../media',fileName);

        return new Promise(async (resolve, reject)=>{
            //代码错误处理
            try {
                // 获取access_token
                const data = await this.fetchAccessToken();
                // 定义请求地址
                const url=`${api.temporary.upload}access_token=${data.access_token}&type=${type}`;

                //素材要以流的方式导入进来
                const forData={
                    // media的格式 是流的格式
                    media:createReadStream(filePath)
                }

                // 发送请求 rp如果有参数forData，就是以form表单的形式发送参数
                //formData里面有个参数media 就是微信上传素材需要的参数media
               const res =await removeProp(({method:'POST',url,json:true,forData}))
                resolve(res); //这里面有mediaid 如果用一次的话就这样，如果要用多次，就要把这个mediaid保存到数据库中，方便以后获取
            }catch (e) {
                //一旦try中的代码出了问题，就会走catch逻辑，处理错误
                reject('uploadTemporaryMaterial上传临时素材出了问题:'+e)
             }
        })
     }

      //获取临时素材  fileName是我们把流转化为文件时候的 用户告诉我们的名字
    getTemporaryMaterial(type,mediaId,fileName){
        const filePath=resolve(__dirname,'../media',fileName);
        // 一会我们也要发送请求 所以包装一层promise对象 套路 async函数
        return new Promise(async (resolve, reject)=>{
            // 获取access_token
            const data =await this.fetchAccessToken();
            // 定义请求地址
            let url=`${api.temporary.get}access_token=${data.access_token}&media_id=${mediaId}`;
            // 视频文件只支持http协议
            if(type==='viedo'){
                let url=url.replace('https://','http://');
                // 发送请求
                const data =await rp({
                    method:'GET',
                    url,
                    json:true
                })
                //返回出去
                resolve(data);
            }else{
                //这是个异步方法 得告诉我们什么时候结束 流文件 的结束是on end
                request(url)
                    .pipe(createWriteStream(filePath))
                    .once('end',()=>{ //文件读取完毕时 ，可读流会自动关闭，一旦关闭触发close事件，从而调用reslove方法，通知外部 文件读取完毕了
                        //告诉用户完了 就行了
                        resolve()
                    })
            }
        })
    }

    //上传永久素材  类型，告诉我上传的是什么东西 要干什么  ,如果是视频的话 还需要body参数
    uploadPermanmentMaterial(type,materail,body){
        return new Promise(async (resolve, reject)=>{
            try {
                //获取access_token
                const data= await this.getAccessToken();
                let options={
                    method:'POST',
                    json:true
                };
                // 定义请求地址url
                if(type='news'){
                    //上传图文消息
                    options.url=`${api.permanment.uploadNews}access_token=${data.access_token}`;
                    options.body= material; //图文消息要传的数据
                }else if(type='pic'){
                    //上传图文消息中的图片
                    options.url=`${api.permanment.uploadimg}access_token=${data.access_token}`;
                    options.formData={
                        media:createReadStream(join(__dirname,'../media',materail)) //图片的名字
                    }
                }else{
                    //其他媒体素材的上传
                    options.url=`${api.permanment.uploadOthers}access_token=${data.access_token}&type=${type}`;
                    options.formData={
                        media:createReadStream(join(__dirname,'../media',materail)) //图片的名字
                    }
                    //如果是视频素材，需要多提交一个表单
                    if(type=='video'){
                        options.body=body;
                    }
                }
                //发送请求
                const result=await rp(options);
                //将返回值返回出去
                resolve(result)
            } catch (e) {
                reject('uploadPermanmentMaterial上传永久素材方法出了问题:'+e)
            }
        })
    }

    //获取永久素材 type不同 有的接收的是json  有的接收的是流 要不同处理
    getPermanmentMaterial(type,mediaId,fileName){
        return new Promise(async (resolve, reject)=>{
            try {
                //获取access_token
                const data=await this.fetchAccessToken();
                // 定义请求地址
                const url=`${api.permanment.get}access_token=${data.access_token}`;
                //发送请求
                let options={
                    method:'POST',
                    url,
                    json:true,
                    body:{
                        media_id:mediaId
                    }
                };
                if(type=='news || video'){
                    const data = await rp(options)
                    //返回出去
                    resolve(data)
                }else{
                    request(options)
                        .pipe(createWriteStream(join(__dirname,'../media',fileName)))
                        .once('end',resolve);
                }
            }catch (e) {
                // 这里不用reject 而用resolve，是为了 不让程序停下来，reject
                resolve('getPermanmentMaterial 下载永久素材出了问题:'+e)
            }
        })
    }

    // 上传方法合并成一个  上传素材（包括上传临时和永久）
    uploadMaterial(type,materail,body,isPermanment=true){
        return new Promise(async (resolve, reject)=>{
            try {
                //获取access_token
                const data= await this.getAccessToken();
                let options={
                    method:'POST',
                    json:true,
                    //素材要以流的方式导入进来
                    forData:{
                        // media的格式 是流的格式
                        media:createReadStream(join(__dirname,'../media',materail))
                    }
                };
                if(isPermanment){
                    /*
                    ·   永久素材的逻辑。
                    */
                    // 定义请求地址url
                    if(type='news'){
                        //上传图文消息
                        options.url=`${api.permanment.uploadNews}access_token=${data.access_token}`;
                        options.body= material; //图文消息要传的数据
                        options.forData=null;
                    }else if(type='pic'){
                        //上传图文消息中的图片
                        options.url=`${api.permanment.uploadimg}access_token=${data.access_token}`;

                    }else{
                        //其他媒体素材的上传
                        options.url=`${api.permanment.uploadOthers}access_token=${data.access_token}&type=${type}`;

                        //如果是视频素材，需要多提交一个表单
                        if(type=='video'){
                            options.body=body;
                        }
                    }

                }else{
                    /*
                    ·   临时素材的逻辑
                    */
                    // 定义请求地址
                    options.url=`${api.temporary.upload}access_token=${data.access_token}&type=${type}`;
                }
                //发送请求
                const result=await rp(options);
                //将返回值返回出去
                resolve(result)
            } catch (e) {
                reject('uploadPermanmentMaterial上传永久素材方法出了问题:'+e)
            }
        })
    }
}

module.exports= Wechat;
//菜单创建 不需要每次都开启，但是需要在修改菜单的时候开启一次
/*(async ()=>{
    const w=new Wechat();
    let result=await w.deleteMenu();
    console.log( '删除：')
    console.log(result);
    result =await w.creatMenu(menu);
    console.log('创建：')
    console.log(result);

    let result_accessToken=await w.fetchAccessToken();
    let result_ticket=await w.fetchTicket();
    // console.log(result_accessToken);
    // console.log(result_ticket);
})()*/


/*
* 测试我们上面的方法
* */
/*let w=new Wechat();

new Promise((resolve,reject)=>{
    w.readAccessToken()
    .then(res=>{
        //本地有文件
         //判断文件是否过期
        if(w.isValidAccessToken(res)){
            resolve(res)
        }else{
            w.getAccessToken()
                .then(res=>{
                    //保存下来 本地文件
                    w.saveAccessToken(res)
                        .then(res=>{
                            resolve(res)
                        })
                })
        }
    })
    .catch(res=>{
        //本地没有文件
        //发送请求获取 access_token
        w.getAccessToken()
            .then(res=>{
                //保存下来 本地文件
                w.saveAccessToken(res)
                    .then(res=>{
                        resolve(res)
                    })
            })
    })
}).then(res=>{
    //这里最终拿到了 没有问题的 res
    console.log('最终结果');
    console.log(res)
})*/



//测试 获取ticket
/*(async ()=>{
    const w=new Wechat();

    let result=await w.fetchTicket();
    console.log(result);
})()*/
